package shared;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;

/*
 * 
 * Encapsulates a jch4t 'message' read in from a socket
 * 
 * The message knows how to read and send a message.
 *
 * @author freeman
 */
public class MessageHandler {

    private byte[] header = null;
    private byte[] message = null;
    private int size = 0;
    private static MessageHandler existingInstance = null;
    private Socket socket = null;
    private InputStream inStream = null;
    private OutputStream outStream = null;
    private String id = null;

    /*
     * creates a message object, does nothing.
     */
    private MessageHandler( String id ) {
        this.id = id;
        header = new byte[HeaderConstants.HEADER_SIZE];
    }

    /*
     * provides singleton access to this message object
     * 
     * @return MessageHandler object to handle sending and receiving of messages
     */
    public static MessageHandler getInstance(String id) {
        /*needs to be fixed...*/
//        if ( existingInstance != null ) {
//            System.err.println("Some instanceis NOT null!");
//            return existingInstance;
//        }
        existingInstance = new MessageHandler(id);
        return existingInstance;
    }

    /*
     * Sets the socket this message will use when sending and receiving
     * messages.
     */
    public boolean setSocket( Socket s ) {
        /*try and close the existing socket, if any*/
        disconnect();

        /*set up the new socket, get streams...*/
        this.socket = s;
        try {
            this.inStream = this.socket.getInputStream();
            this.outStream = this.socket.getOutputStream();
        } catch ( IOException ioe ) {
            System.err.println( "MessageHandler: Could not get input and/or"
                    + " output streams from socket." );
            return false;
        }
        return true;

    }

    /*
     * reads a message from the socket, if no socket is specified, 
     * this method returns false
     * 
     * @return true if the message could be read, false otherwise
     */
    public boolean read() {
        int lengthRead;

        /*check to make sure the socket and instream is not null*/
        if ( this.socket == null ) {
            System.err.println( "MessageHandler: No socket assigned for the"
                    + " message." );
            return false;
        }
        if ( this.inStream == null ) {
            System.err.println( "MessageHandler: No input stream assigned for"
                    + " the message" );
            return false;
        }
        try {
            /*read size*/
            this.size = inStream.read();
            
            /*validate size*/
            if ( this.size > 10000 || this.size < 1 ) {
                System.err.println("MessageHandler: Error: Size attempted to"
                        + " read was " + this.size);
                return false;
            }

            /*read in the header, checking its size*/
            lengthRead = inStream.read( this.header );
            if ( lengthRead != HeaderConstants.HEADER_SIZE ) {
                System.err.println( "MessageHandler: Warning: Header size"
                        + " read was"
                        + " " + lengthRead + ", "
                        + "expected " + HeaderConstants.HEADER_SIZE );
                return false;
            }

            /*allocate and read message */
            message = new byte[this.size];
            lengthRead = inStream.read( this.message, 0, this.size );

            if ( lengthRead != this.size ) {
                System.err.println( "MessageHandler: Warning: Message read was"
                        + " not equal to"
                        + " size expected." );
            }
        } catch ( IOException ioe ) {
            System.err.println( "MessageHandler: Reading failed: "
                    + ioe.getMessage() );

            /*reset the socket and IO stream items; the socket will need to be 
             re-established.*/
            disconnect();
            socket = null;
            inStream = null;
            outStream = null;
            return false;
        }
        return true;
    }
    
    /*
     * Terminates this messagehandler by closing its socket.
     */
    public void terminate() {
        System.err.println("MessageHandler: Closing input stream...");
        try {
            inStream.close();
        } catch ( IOException ex ) {
            System.err.println("Unable to close the stream: " + ex.getMessage() );
        }
    }

    /*
     * writes a message to the socket associeted with this object.
     * 
     * @param wHeader: the ehader of the message to be written
     * @param wMessage: the message to be written
     * 
     * @return: false if the message cannot be written, true otherwise
     */
    public boolean write( byte[] wHeader, byte[] wMessage ) {
        if ( this.socket == null ) {
            System.err.println( "MessageHandler: No socket assigned for the"
                    + " message." );
            return false;
        }
        if ( this.outStream == null ) {
            System.err.println( "MessageHandler: No output stream assigned"
                    + " for the message" );
            return false;
        }
        try {
            outStream.write( wMessage.length );
            outStream.flush();
            outStream.write( wHeader );
            outStream.flush();
            outStream.write( wMessage, 0, wMessage.length );
            outStream.flush();
        } catch ( IOException ioe ) {
            System.err.println( "MessageHandler: Could not send the message: "
                    + ioe.getMessage() );
            return false;
        }
        return true;
    }

    /*
     * Disconnects the socket.
     */
    public void disconnect() {
        if ( socket != null && socket.isConnected() ) {
            try {
                socket.close();
            } catch ( IOException ex ) {
                System.err.println( "MessageHandler: Unable to close socket." );
            }
        }
    }

    /*
     * returns the string representation of the message
     * 
     * @return the string representation of a message
     */
    public String getMessageString() {
        if ( message == null ) {
            return null;
        }
        return new String( message );
    }

    /*
     * returns the byte array represntation of the message
     * 
     * @return the byte array representation of a message
     */
    public byte[] getMessageBytes() {
        return message;
    }

    /*
     * returns the size of the message
     * 
     * @return the size of the message.
     */
    public int getSize() {
        return size;
    }

    /*
     * return the header of the message
     * 
     * @return the byte header.
     */
    public byte[] getHeader() {
        return header;
    }

    /*
     * returns true if the socket is connected, false otherwise.
     */
    public boolean isConnected() {
        return this.socket != null && this.socket.isConnected();
    }

    /*
     * Checks to see if the header o this message and the header given as a 
     * parameter are the same.
     * 
     * @param header: the header to check against.
     * 
     * @return true if the headers re the same, false otherwise.
     */
    public boolean headerEquals( byte[] header ) {
        /*check to make sure the headers are the same length*/
        if ( header.length != this.header.length ) {
            return false;
        }

        /*make sure the contents of each header are the same*/
        for ( int i = 0; i < header.length; i++ ) {
            if ( header[i] != this.header[i] ) {
                return false;
            }
        }

        return true;
    }

    /*
     * returns a string rep of the header
     * 
     * @return a string rep of the header
     */
    public String headerString() {
        StringBuilder buf = new StringBuilder();

        /*append header*/
        for ( int i = 0; i < this.header.length; i++ ) {
            buf.append( "[" ).append( this.header[i] ).append( "] " );
        }

        return buf.toString();
    }
    /*
     * Checks to see if the object viven as argument is the same as this 
     * messagehandler.
     * 
     * @param the object to check against
     * 
     * @return true if the object given as an argument is the same as this
     * object, false otherwise.
     */

    @Override
    public boolean equals( Object msgHandlerObj ) {

        /*make sure they are the same object*/
        if ( !( msgHandlerObj instanceof MessageHandler ) ) {
            return false;
        }

        MessageHandler messageHandler = (MessageHandler) msgHandlerObj;

        /*check size*/
        if ( messageHandler.getSize() != this.size ) {
            return false;
        }

        /*check header*/
        for ( int i = 0; i < this.header.length; i++ ) {
            if ( this.header[i] != messageHandler.getHeader()[i] ) {
                return false;
            }
        }

        /*check message*/
        for ( int i = 0; i < this.header.length; i++ ) {
            if ( this.header[i] != messageHandler.getMessageBytes()[i] ) {
                return false;
            }
        }

        return true;
    }

    /*
     * Calculates the hashcode for this object.
     * 
     * @return int hashcode.
     */
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 37 * hash + Arrays.hashCode( this.header );
        hash = 37 * hash + Arrays.hashCode( this.message );
        hash = 37 * hash + this.size;
        return hash;
    }

    /*
     * Returns a string representation of this object.
     * 
     * @return a string that represents this object.
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();

        buf.append( "Size: " ).append( this.size ).append( "\n" );

        /*append header*/
        buf.append( "Header: " + "\n" );

        for ( int i = 0; i < this.header.length; i++ ) {
            buf.append( "byte " ).append( i ).append( ": " ).
                    append( this.header[i] ).append( "\n" );
        }

        /*append message*/
        buf.append( "Message: " + "\n" );

        for ( int i = 0; i < this.message.length; i++ ) {
            buf.append( "byte " ).append( i ).append( ": " ).
                    append( this.message[i] ).append( "\n" );
        }
        return buf.toString();
    }
}
